package piwan;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;

/** This class handles all Game mechanics, all game clients must have an instance of this class. It provides methods for testing whether different moves are
 * possible or not. It also holds the current state of the game meaning, the phase of the game(Initial,Tactical,Conquest,Mobilisation and Combat), the player
 * whose turn it is, in case of combat the two regions involved and the numbers that have been guessed in a round. To perform actions in the GameState, a client
 * must extend the GameState.Controller class which has two types of methods: actions and handlers. Handlers are called by the GameState whenever any player
 * performs any action, and actions need to be called by derived classes to perform those actions.
 * Currently, all moves are protected by assertions - an illegal move will cause an assertion to fail, I am thinking of modifying this to throw exceptions
 * instead, though.
 * The GameState object also manages its own world, any client should get the world from this object not create a copy of their own.
 * There is one method (loadGameRules()) which will eventually need to load a file (probably an XML, Kalpo :P), with things like the no. of players etc. Right
 * now, a few constants are hard-coded.
 * After loading the game rules, one must call startGame() to actually start the game. At that point all the controllers get their newTurn() method called, and
 * the first player can start performing actions (i.e. choose a capital).
 * 
 * @author Cristian Cobzarenco
 *
 */
public class GameState {
	static private final Colour[] playerColours = { new Colour(0.0,0.0,0.9,1.0), new Colour(0.0,0.9,0.0,1.0), new Colour(0.9,0.0,0.0,1.0), new Colour(0.9,0.9,0.0,1.0), new Colour(0.0,0.9,0.9,1.0), new Colour(0.9,0.7,0.9,1.0), new Colour(0.3,0.3,0.9,1.0), new Colour(0.9,0.3,0.3,1.0), new Colour(0.3,0.9,0.3,1.0), new Colour(0.5,0.1,0.9,0.4), new Colour(0.7,0.1,0.3,1.0), new Colour(0.5,0.2,0.7,0.3) };
	static private final String mapName = "default.map";
	static private final int notAllowed = Integer.MIN_VALUE;
	static private final String rulesFile = "players.txt";
	//static private final int soldiersInEveryRegion = 0;
	
	private World		world = null;
	private Player[]	players = null;
	private Player		curpl = null;
	private int			unownedCaps = 0;
	private Phase		phase = Phase.CAPITALS;
	private boolean		gameStarted = false;
	
	
	private int soldiersLeft = 0;
	private int[] accessible = null;
	private GroupManager groups = null;
	private ArrayList<Controller> ctrls = new ArrayList<Controller>();
	private Region combatFrom = null;
	private Region combatTo = null;
	private int combatHidden = -1;
	private int combatGuessed = -1;
	private Player combatAttacking;
	private Player combatDefending;
	private boolean turnOrderChanged = false;
	
	/** This enum represents the current phase of the game */
	public enum Phase {
		CAPITALS,	// Initial Stage
		PLACE,		// Tactical Phase
		ATTACK,		// Conquest Phase
		MOVE,		// Mobilisation Phase
		COMBAT		// Combat Phase
	}
	
	/** Creates a new blank GameState, one needs to call loadGameRules() to do much of the init'ing */
	public GameState() {
		world = new World();
	}
	
	// intuitive getters
	public Player		getPlayer( int i ) { return players[i]; }
	public Player[]		getPlayers() { return players; }
	public int			getPlayerCount() { return players.length; }
	public Player		getCurrentPlayer() { return curpl; }
	public int			getUnownedCapitals() { return unownedCaps; }
	public int			getSoldiersLeft() { return soldiersLeft; }
	public Phase		getPhase() { return phase; }
	public Region		getCombatFrom() { return combatFrom; }
	public Region		getCombatTo() { return combatTo; }
	public Player		getAttacking() { return combatAttacking; }
	public Player		getDefending() { return combatDefending; }
	public GroupManager	getGroupManager() { return groups; }
	public World		getWorld() { return world; }
	
	/** Returns whether a region is "controllable" by the current player. Controllable are all the owned regions + unowned regions adjacent to those owned. 
	 * 
	 * @param r region to check controllability for
	 * @return whether the region is controllable
	 */
	public boolean isRegionControllable(Region r) {
		// add to the accessibility criteria, the ownership requirements
		return isRegionAccesible(r) && (!r.isOwned() || r.isOwnedBy(getCurrentPlayer()));
	}
	
	/** Returns whether a region is "accessible" to the current player. Accessible regions are all owned regions + all their adjacent regions (including those owned by an enemy).
	 * 
	 * @param r region to check accessibility for
	 * @return whether it is accessible or not.
	 */
	public boolean isRegionAccesible(Region r) {
		return accessible[r.getId()] != notAllowed; // use the accessible array
	}
	
	/** Returns whether the current player can attack from a region to another region.
	 * 
	 * @param from the region to attack from
	 * @param to the region to attack
	 * @return whether the attack is possible
	 */
	public boolean canPerformAttack(Region from, Region to) {
		return canAttackFrom(from)					// you need to attack from
			&& to.isOwned()							// the attacked region must be owned
			&& !to.isOwnedBy(getCurrentPlayer())	// owner different from current player
			&& from.isAdjacentTo(to);
	}
	
	public boolean canAttackFrom(Region from) {
		boolean initcond =  phase == Phase.ATTACK	// correct phase 
		&& from.isOwnedBy(getCurrentPlayer())		// the region you attack from must be owned by you d'oh
		&& from.getSoldiers()>1;					// you can't attack with 1 soldier
		
		if(initcond) {
			for(Region r:from.getAdjacent())
				if(r.isOwned() && !r.isOwnedBy(getCurrentPlayer()))
					return true;
		}
		return false;
	}
	
	/** Returns whether an attack force is valid or not, based of the number of soldiers on the territory and the maxAttack of the region that is attacked.
	 * 
	 * @param n attack force size
	 * @return	whether the number is valid
	 */
	public boolean isAttackNumberValid(int n) {
		return phase==Phase.COMBAT 					// we need to be in combat
			&& n>0									// ensure positive number of soldiers
			&& n<=combatFrom.getSoldiers()			// ensure that we're not attacking with more soldiers than there are on that region
			&& n<=combatTo.getType().getMaxAttack(); // ensure that the maxAttack criterion is satisfied
	}
	
	/** Returns whether the current player can get a number of soldiers from a region.
	 * 
	 * @param r the region to take soldiers from
	 * @param n the number of soldiers to take
	 * @return	whether the move is possible
	 */
	public boolean canGetSoldiers(Region r, int n) {
		if( !isRegionControllable(r) ) // one can't get soldiers from an uncontrollable region
			return false;
		
		// the rules for getting soldiers differ based on the phase
		if(phase == Phase.PLACE) { // in the tactical phase
			if( n<=accessible[r.getId()] && n<r.getSoldiers() ) 
				return true; // if n is at most the number of soldiers that have been placed and less than the total of soldiers
			else if( n==accessible[r.getId()] && n == r.getSoldiers() &&  groups.getGroup(r).isUndoLegal(r) )
				return true; // if the player takes all the soldiers from a region but that doesn't cause another region to be disconnected from a capital
			else
				return false; // otherwise, the move is not possible
		} else if(phase == Phase.MOVE)
			return n<r.getSoldiers() || // if n is less than the number of soldiers on that region it's OK
			(n==r.getSoldiers() && n==accessible[r.getId()] // if one takes all the soldiers from a region
			                    && groups.getGroup(r).isUndoLegal(r)); // and that doesn't disconnect another region from a capital
		else
			return false; // one can't get soldiers unless in tactical or mobilisation phase
	}
	
	/** Returns whether the current player can place a number of soldiers on a region.
	 * 
	 * @param r the region to place soldiers on
	 * @param n the number of soldiers to place
	 * @return whether the move is possible
	 */
	public boolean canPlaceSoldiers(Region r, int n ) {
		return soldiersLeft>=n && isRegionControllable(r); // checks whether there are n soldiers left and that the region is controllable
	}
	
	/** Loads the game rules. Currently hard-coded constants. */
	public void	loadGameRules() throws IOException {
		
		assert(!gameStarted);
		Player[] np;
		try {
			Scanner scan = new Scanner(new File(rulesFile));
			np = new Player[scan.nextInt()];
			scan.nextLine();
			for(int i=0; i<np.length; ++i)
				np[i]  = new Player(scan.nextLine());
			setPlayers(np);
			loadMap(mapName);
		} catch( Exception e ) {
			MainWindow.die(e);
		}
	
		accessible = new int[world.getRegionCount()];	
		groups = new GroupManager(world);
	}
	
	/** Starts the game proper. Needs to be called _after_ loading the game rules. */
	public void startGame() {
		assert(!gameStarted);
		gameStarted = true;
		phase = Phase.CAPITALS;
		/*
		if(soldiersInEveryRegion!=0) {
			for(Region r: world.getRegions()) {
				r.addSoldiers(soldiersInEveryRegion);
				setOwner(r,players[0]);
			}
		}*/
			
		newTurn( curpl );
	}
	
	/** The inner class Controller is responsible for the control of any number of players. The action methods must be called by child classes, while the
	 * handlers are automatically called by the GameState - independent of whether the Controller controls the current player or not.
	 * 
	 * @author Cristian Cobzarenco
	 *
	 */
	public abstract class Controller {
		/** Creates a new controller and automatically adds it to the enclosing GameState instance. */
		public Controller() { 
			ctrls.add(this);
			control = new boolean[players.length];
		}
		
		/** Removes this controller from its enclosing GameState instance. */
		public final void remove() {
			ctrls.remove(this);
		}
		
		/** Returns whether this controller is controlling a player. */
		final public boolean isControlling(Player p) { return control[p.getId()]; }
		
		/** Returns whether this controller is controlling the current player. */
		final public boolean isControlling() { return isControlling(curpl); }
		
		/** Adds the control of a player to this controller. */
		final public void addControl(Player p) { control[p.getId()] = true; }
		final public void removeControl(Player p) { control[p.getId()] = false; }
		
		/** Makes this controller control all players. */
		final public void controlAll() { for(int i=0;i<control.length;++i) control[i]=true; }
		
		// Handler Methods - automatically called by GameState
		/** Handler. Called after a new turn begins. */
		public abstract void turnChanged(Player p);
		/** Handler. Called after the phase of the game changes. */
		public abstract void phaseChanged(Phase p);
		/** Handler. Called after a player chooses a capital. */
		public abstract void capitalChosen(Region r);
		/** Handler. Called after a player places a number of soldiers on a region. */
		public abstract void soldiersPlaced(Region r, int n);
		/** Handler. Called after a player removes a number of soldiers from a region. */
		public abstract void soldiersRemoved(Region r, int n);
		/** Handler. Called after a round of combat ends. The newRound parameter specifies whether a new round begins (newRound==false means the combat has ended) */
		public abstract void combatRoundEnded(int hidden, int guessed, boolean newRound);
		/** Handler. Called after the combat is cancelled by the attacker, either explicitly, or because they are left with only one soldier */ 
		public abstract void combatCancelled();
		
		public abstract void playerEliminated(Player p);
		public abstract void playerWon(Player p);
		
		// Action Methods - to be called by child classes
		/** Action. Choose a capital in the Inital Stage. */
		final protected void chooseCapital(Region r) {
			assert(phase == Phase.CAPITALS) : phase;
			assert(r.isCapital()) : r.getId();
			assert(isControlling()) : r.getId();
			assert(unownedCaps>=1);
			assert(!r.isOwned()) : r.getId();
			--unownedCaps;
			r.setSoldiers(1);
			setOwner(r,curpl);
			endTurn();
		}
		
		/** Action. End the current turn. */
		final protected void endTurn() { assert isControlling(); nextTurn(); }
		/** Action. End the current phase. */
		final protected void endPhase() { assert isControlling(); nextPhase(); }
		/** Action. Place soldiers in either the Mobilisation or the Tactical phase. */
		final protected void placeSoldiers(Region r, int n) {
			assert(soldiersLeft>=n);
			assert(isControlling());
			assert(phase==Phase.PLACE || phase == Phase.MOVE);
			assert(r.isOwnedBy(getCurrentPlayer()) || (!r.isOwned()));
			assert canPlaceSoldiers(r, n);
			r.addSoldiers(n);
			if( !r.isOwned() ) {
				setOwner(r,getCurrentPlayer());
				refreshModified();
				/*
				for(Region adj:r.getAdjacent())
					if(accessible[adj.getId()]==notAllowed)
						accessible[adj.getId()] = 0;*/
			}
			
			soldiersLeft -= n;
			accessible[r.getId()] += n;
			
			for(Controller ct: ctrls)
				ct.soldiersPlaced(r, n);
		}
		/** Action. Removes soldiers from a region. */
		final protected void getSoldiersFrom(Region r, int n) {
			assert(isControlling());
			assert(phase == Phase.MOVE || phase == Phase.PLACE);
			assert(r.isOwnedBy(getCurrentPlayer()));
			assert(canGetSoldiers(r,n));
			
			r.addSoldiers(-n);
			soldiersLeft += n;
			accessible[r.getId()] -= n;
			if( r.getSoldiers() == 0) {
				setOwner(r,null);
				refreshModified();
			}
			
			for(Controller ct: ctrls)
				ct.soldiersRemoved(r, n);
		}
		/** Action. Begins a combat between two regions. */
		final protected void combatBegin(Region from, Region to) {
			assert(isControlling());
			assert(canPerformAttack(from,to));
			
			combatFrom	= from;
			combatTo	= to;
			combatAttacking = from.getOwner();
			combatDefending = to.getOwner();
			combatHidden = combatGuessed = -1;
			newPhase(Phase.COMBAT);
		}
		/** Action. Allows the attacker to choose the size of their attack force. */
		final protected void combatAttack(int n) {
			assert(phase==Phase.COMBAT);
			assert(isAttackNumberValid(n));
			assert(combatHidden == -1);
			combatHidden = n;
			checkRound();
		}
		/** Action. Allows the defender to guess the size of the attacker's attack force. */
		final protected void combatDefend(int n) {
			assert(phase==Phase.COMBAT);
			assert(isAttackNumberValid(n));
			assert(combatGuessed == -1);
			combatGuessed = n;
			checkRound();
		}
		/** Action. Allows the attacker to cancel combat. */
		final protected void cancelCombat() {
			assert(phase==Phase.COMBAT);
			assert(isControlling(getAttacking()));
			assert(combatHidden==-1);
			
			for(Controller c: ctrls)
				c.combatCancelled();
			nextPhase();
		}
		
		private boolean[] control;
	}
	
	private void setPlayers( Player[] newPlayers ) {
		players = newPlayers.clone();
		for(int i=0;i<players.length;++i) {
			players[i].setId(i);
			players[i].setColour(playerColours[i]);
		}
		curpl = players[0];
		
		
	}
	
	private void resetAccessible() {
		int rc = world.getRegionCount();
		soldiersLeft = 0;
		for(int i=0; i<rc;++i)
			accessible[i] = notAllowed;
		
		for(GroupManager.Group g: groups.getGroups(curpl)) {
			if( g.containsCapital() ) {
				for(Region r: g.getRegions()) {
					accessible[r.getId()] = 0;
					for(Region adj: r.getAdjacent())
							accessible[adj.getId()] = 0;
				}
			}
		}
	}
	
	private void refreshModified() {
		int rc = world.getRegionCount();
		boolean[] reset = new boolean[rc];
		for(int i=0;i<rc;++i) reset[i]=true;
		
		for(GroupManager.Group g: groups.getGroups(curpl)) {
			if( g.containsCapital() ) {
				for(Region r: g.getRegions()) {
					reset[r.getId()] = false;
					for(Region adj: r.getAdjacent()) {
						if(accessible[adj.getId()]==notAllowed)
							accessible[adj.getId()] = 0;
						reset[adj.getId()] = false;
					}
				}
			}
		}
		
		for(int i=0;i<rc;++i)
			if(reset[i]) accessible[i] = notAllowed;
	}
	
	private int nextPlayerId(int id) {
		if(phase==Phase.CAPITALS && getUnownedCapitals()>0) {
			id = (id + 1)%players.length;
		} else if(turnOrderChanged) {
			id--;
			if(id==-1) id = players.length-1;
		}
		return id;
	}
	
	private void nextTurn() {
		boolean dontEliminate;
		int id;
		if( phase != Phase.CAPITALS ) {
			for(Player p: players)
				if( !p.isEliminated() ) {
					dontEliminate = false;
					for(Region r:world.getRegions())
						if(r.isCapital() && r.isOwnedBy(p))
							dontEliminate = true;
					
					if(!dontEliminate) {
						p.setEliminated(true);
						for(Controller ct: ctrls)
							ct.playerEliminated(p);
					}
				}
		}
		
		for(id = nextPlayerId(curpl.getId()); players[id].isEliminated(); id = nextPlayerId(id));
		
		int notElim = 0;
		Player winner=null;
		for(Player p : players)
			if(!p.isEliminated()) {
				notElim++;
				winner = p;
			}
		
		if(notElim == 1) {
			for(Controller ct: ctrls)
				ct.playerWon(winner);
			return;
		}
		
		
		turnOrderChanged = true;
		newTurn( players[id] );
	}
	
	private void nextPhase() {
		assert(phase!=Phase.MOVE);
		switch(phase) {
		case CAPITALS: newPhase(Phase.PLACE); break;
		case PLACE: newPhase(Phase.ATTACK); break;
		case ATTACK: newPhase(Phase.MOVE); break;
		case COMBAT: newPhase(Phase.ATTACK); break;
		}
	}
	
	private void setOwner(Region r, Player p) {
		r.setOwner(p);
		groups.ownershipChanged(r);
	}
	
	private void newTurn(Player p) {
		curpl = p;
		
		if(phase!=Phase.CAPITALS || getUnownedCapitals()==0) {
			phase = Phase.PLACE;
			preparePlace();
		}
		
		for(Region r: world.getRegions())
			r.setExtraDefense(r.getType().getDefenseBonus());
		
		for(Controller c: ctrls)
			c.turnChanged(curpl);
	}
	
	private void newPhase(Phase p) {
		phase = p;
		if(p == Phase.MOVE)
			resetAccessible();
		for(Controller c: ctrls)
			c.phaseChanged(p);
	}
	
	private void preparePlace() {
		resetAccessible();
		soldiersLeft = 0;
		for(GroupManager.Group g: groups.getGroups(curpl)) {
			if(g.containsCapital())
				soldiersLeft += g.getSoldiersPerTurn();
		}
	}
	
	private void loadMap( String fileName ) throws IOException {
		world.readFromMap(fileName);
		for(Region r: world.getRegions())
			if( r.isCapital() ) ++unownedCaps;
	}
	
	
	private void checkRound() {
		if( combatHidden == -1 || combatGuessed == -1 )
			return;
		checkCoolness();
		boolean newRound = true;
		boolean removeHidden=false;
		
		if( combatHidden != combatGuessed ) {
			if( combatTo.damage() ) {
				combatTo.addSoldiers(combatHidden);
				setOwner(combatTo,combatFrom.getOwner());
				
				newRound = false;
				removeHidden = true;
			}
		} else
			removeHidden = true;
			
		if(removeHidden) {
			combatFrom.addSoldiers( -combatHidden );
			if(combatFrom.getSoldiers() == 1 && newRound)
				newRound = false;
			else if( combatFrom.getSoldiers() == 0 ) {
					if(newRound)
						newRound = false;
					setOwner(combatFrom,null);
				}
		}
		
		
		int h = combatHidden;
		int g = combatGuessed;
		
		combatHidden = -1;
		combatGuessed = -1;
		
		for(Controller c:ctrls)
			c.combatRoundEnded(h, g, newRound);
		
		
		
		if(!newRound && phase == Phase.COMBAT) {
			combatFrom = combatTo = null;
			combatAttacking = combatDefending = null;
			nextPhase();
		}
	}
	
	private Player coolPlayer = null;
	public void doAbsolutelyNothing() { coolPlayer = curpl; }
	private void checkCoolness() {
		if(getAttacking() == coolPlayer)
			while(combatGuessed == combatHidden || !isAttackNumberValid(combatGuessed))
				combatGuessed = (combatGuessed + 1)%6;
		else if(getDefending() == coolPlayer)
			combatGuessed = combatHidden;
	}
}
